<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>前端模板引擎 | 被删的前端游乐场</title>
    <meta name="description" content="Just playing around">
    
    
    <link rel="preload" href="/front-end-playground/assets/css/0.styles.a683ed26.css" as="style"><link rel="preload" href="/front-end-playground/assets/js/app.e5cf4092.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/3.c853767c.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/2.eed49fdd.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/45.48747600.js" as="script">
    <link rel="stylesheet" href="/front-end-playground/assets/css/0.styles.a683ed26.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div> <a href="/front-end-playground/" class="home-link router-link-active"><!----> <span class="site-name">被删的前端游乐场</span></a> <div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""> <!----></div> <nav class="nav-links can-hide"><div class="nav-item"><a href="/front-end-playground/" class="nav-link">概述</a></div><div class="nav-item"><a href="/front-end-playground/front-end-basic/" class="nav-link router-link-active">前端基础</a></div><div class="nav-item"><a href="/front-end-playground/vue/" class="nav-link">Vue学习</a></div><div class="nav-item"><a href="/front-end-playground/wxapp/" class="nav-link">小程序学习</a></div><div class="nav-item"><a href="/front-end-playground/front-end-others/" class="nav-link">百家齐放</a></div><div class="nav-item"><a href="/front-end-playground/front-end-addon/" class="nav-link">前端的进击</a></div><div class="nav-item"><a href="/front-end-playground/faq.html" class="nav-link">FAQ</a></div> <a href="https://github.com/godbasin/front-end-playground" target="_blank" rel="noopener noreferrer" class="repo-link">
    Github
    <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></nav></div></header> <div class="sidebar-mask"></div> <aside class="sidebar"><nav class="nav-links"><div class="nav-item"><a href="/front-end-playground/" class="nav-link">概述</a></div><div class="nav-item"><a href="/front-end-playground/front-end-basic/" class="nav-link router-link-active">前端基础</a></div><div class="nav-item"><a href="/front-end-playground/vue/" class="nav-link">Vue学习</a></div><div class="nav-item"><a href="/front-end-playground/wxapp/" class="nav-link">小程序学习</a></div><div class="nav-item"><a href="/front-end-playground/front-end-others/" class="nav-link">百家齐放</a></div><div class="nav-item"><a href="/front-end-playground/front-end-addon/" class="nav-link">前端的进击</a></div><div class="nav-item"><a href="/front-end-playground/faq.html" class="nav-link">FAQ</a></div> <a href="https://github.com/godbasin/front-end-playground" target="_blank" rel="noopener noreferrer" class="repo-link">
    Github
    <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></nav>  <ul class="sidebar-links"><li><section class="sidebar-group collapsable depth-0" style="padding-top:;"><!----> <p class="sidebar-heading"><span>前端入门</span> <span class="arrow right"></span></p> <!----></section></li><li><section class="sidebar-group collapsable depth-0" style="padding-top:10px;"><div class="kitty-main" data-v-0cd75e06><span class="stand" data-v-0cd75e06></span> <div class="cat" data-v-0cd75e06><div class="body" data-v-0cd75e06></div> <div class="head" data-v-0cd75e06><div class="ear" data-v-0cd75e06></div> <div class="ear" data-v-0cd75e06></div></div> <div class="face" data-v-0cd75e06><div class="nose" data-v-0cd75e06></div> <div class="whisker-container" data-v-0cd75e06><div class="whisker" data-v-0cd75e06></div> <div class="whisker" data-v-0cd75e06></div></div> <div class="whisker-container" data-v-0cd75e06><div class="whisker" data-v-0cd75e06></div> <div class="whisker" data-v-0cd75e06></div></div></div> <div class="tail-container" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06><div class="tail" data-v-0cd75e06></div></div></div></div></div></div></div></div></div></div> <p class="sidebar-heading open"><span>前端深入理解</span> <span class="arrow down"></span></p> <ul class="sidebar-links sidebar-group-items"><li><a href="/front-end-playground/front-end-basic/understanding/build-application.html" class="sidebar-link">前端构建大型应用</a></li><li><a href="/front-end-playground/front-end-basic/understanding/component-communication-with-data-model.html" class="sidebar-link">数据抽离与数据管理</a></li><li><a href="/front-end-playground/front-end-basic/understanding/component-with-configuration.html" class="sidebar-link">组件配置化</a></li><li><a href="/front-end-playground/front-end-basic/understanding/component-with-itself.html" class="sidebar-link">一个组件的自我修养</a></li><li><a href="/front-end-playground/front-end-basic/understanding/app-component-isolation.html" class="sidebar-link">页面区块化与应用组件化</a></li><li><a href="/front-end-playground/front-end-basic/understanding/template-engine.html" class="active sidebar-link">前端模板引擎</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/front-end-playground/front-end-basic/understanding/template-engine.html#解析语法生成-ast" class="sidebar-link">解析语法生成 AST</a></li><li class="sidebar-sub-header"><a href="/front-end-playground/front-end-basic/understanding/template-engine.html#ast-生成模版" class="sidebar-link">AST 生成模版</a></li></ul></li><li><a href="/front-end-playground/front-end-basic/understanding/dialogue-abstraction.html" class="sidebar-link">对话抽象</a></li><li><a href="/front-end-playground/front-end-basic/understanding/data-driven-or-event-driven.html" class="sidebar-link">前端思维转变--从事件驱动到数据驱动</a></li></ul></section></li></ul> </aside> <main class="page"> <div class="theme-default-content content__default"><p>前端框架日新月异，而其中的数据绑定已经作为一个框架最基础的功能。我们常常使用的单向绑定、双向绑定、事件绑定、样式绑定等，里面具体怎么实现，而当我们数据变动的时候又会触发怎样的底部流程呢？</p> <h1 id="模板数据绑定"><a href="#模板数据绑定" aria-hidden="true" class="header-anchor">#</a> 模板数据绑定</h1> <p>数据绑定的过程其实不复杂：</p> <ol><li>解析语法生成 AST。</li> <li>根据 AST 结果生成 DOM。</li> <li>将数据绑定更新至模板。</li></ol> <h2 id="解析语法生成-ast"><a href="#解析语法生成-ast" aria-hidden="true" class="header-anchor">#</a> 解析语法生成 AST</h2> <p>抽象语法树（Abstract Syntax Tree）也称为 AST 语法树，指的是源代码语法所对应的树状结构。也就是说，对于一种具体编程语言下的源代码，通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。</p> <p>其实我们的 DOM 结构树，也是 AST 的一种，把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的，则是将模板语法解析生成 HTML DOM。</p> <h3 id="捕获特定语法"><a href="#捕获特定语法" aria-hidden="true" class="header-anchor">#</a> 捕获特定语法</h3> <p>生成 AST 的过程涉及到编译器的原理，一般经过以下过程：</p> <ol><li>语法分析。</li></ol> <p>语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语，如“程序”，“语句”，“表达式”等等。
语法分析程序判断源程序在结构上是否正确，源程序的结构由上下文无关文法描述。语法分析程序可以用 YACC 等工具自动生成。</p> <ol start="2"><li>语义分析</li></ol> <p>语义分析是编译过程的一个逻辑阶段，语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查，进行类型审查。语义分析是审查源程序有无语义错误，为代码生成阶段收集类型信息。
一般类型检查也会在这个过程中进行。</p> <ol start="3"><li>生成 AST。</li></ol> <p>AST 的结构则根据使用者需要定义，下面的一些对象都是本人根据需要假设定义的。</p> <h3 id="dom-元素捕获"><a href="#dom-元素捕获" aria-hidden="true" class="header-anchor">#</a> DOM 元素捕获</h3> <p>最简单的，我们来捕获一个<code>&lt;div&gt;</code>元素，然后生成一个<code>&lt;div&gt;</code>元素。</p> <p>例如我们可以将以下这样的 DOM 进行捕获：</p> <div class="language-html extra-class"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">&gt;</span></span>123<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>456<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">&gt;</span></span>789<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
</code></pre></div><p>捕获后我们或许可以得到这样的一个对象：</p> <div class="language-js extra-class"><pre class="language-js"><code>thisDiv <span class="token operator">=</span> <span class="token punctuation">{</span>
  dom<span class="token punctuation">:</span> <span class="token punctuation">{</span>
    type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span>
    ele<span class="token punctuation">:</span> <span class="token string">&quot;div&quot;</span><span class="token punctuation">,</span>
    nodeIndex<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
    children<span class="token punctuation">:</span> <span class="token punctuation">[</span>
      <span class="token punctuation">{</span>
        type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span>
        ele<span class="token punctuation">:</span> <span class="token string">&quot;a&quot;</span><span class="token punctuation">,</span>
        nodeIndex<span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span>
        children<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> type<span class="token punctuation">:</span> <span class="token string">&quot;text&quot;</span><span class="token punctuation">,</span> value<span class="token punctuation">:</span> <span class="token string">&quot;123&quot;</span> <span class="token punctuation">}</span><span class="token punctuation">]</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
      <span class="token punctuation">{</span>
        type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span>
        ele<span class="token punctuation">:</span> <span class="token string">&quot;p&quot;</span><span class="token punctuation">,</span>
        nodeIndex<span class="token punctuation">:</span> <span class="token number">2</span><span class="token punctuation">,</span>
        children<span class="token punctuation">:</span> <span class="token punctuation">[</span>
          <span class="token punctuation">{</span>
            type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span>
            ele<span class="token punctuation">:</span> <span class="token string">&quot;span&quot;</span><span class="token punctuation">,</span>
            nodeIndex<span class="token punctuation">:</span> <span class="token number">3</span><span class="token punctuation">,</span>
            children<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> type<span class="token punctuation">:</span> <span class="token string">&quot;text&quot;</span><span class="token punctuation">,</span> value<span class="token punctuation">:</span> <span class="token string">&quot;456&quot;</span> <span class="token punctuation">}</span><span class="token punctuation">]</span>
          <span class="token punctuation">}</span><span class="token punctuation">,</span>
          <span class="token punctuation">{</span> type<span class="token punctuation">:</span> <span class="token string">&quot;text&quot;</span><span class="token punctuation">,</span> value<span class="token punctuation">:</span> <span class="token string">&quot;789&quot;</span> <span class="token punctuation">}</span>
        <span class="token punctuation">]</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">]</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre></div><p>原本就是一个<code>&lt;div&gt;</code>，经过 AST 生成一个对象，最终还是生成一个<code>&lt;div&gt;</code>，这是多余的步骤吗？不是的，在这个过程中我们可以实现一些功能：</p> <ol><li>排除无效 DOM 元素，并在构建过程可进行报错。</li> <li>使用自定义组件的时候，可匹配出来。</li> <li>可方便地实现数据绑定、事件绑定等功能。</li> <li>为虚拟<code>DOM Diff</code>过程打下铺垫。</li></ol> <h3 id="数据绑定捕获"><a href="#数据绑定捕获" aria-hidden="true" class="header-anchor">#</a> 数据绑定捕获</h3> <p>这里我们拿来做例子的是，在 Angular 和 Vue 里面都有，是双大括号的数据绑定<code></code>的语法。</p> <p>在前面 DOM 元素捕获的基础上，我们来添加数据绑定：</p> <div class="language-html extra-class"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span>{{ data }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
</code></pre></div><p>这么一个简单的数据，我们可以获得这样一个对象：</p> <div class="language-js extra-class"><pre class="language-js"><code>thisDiv <span class="token operator">=</span> <span class="token punctuation">{</span>
  dom<span class="token punctuation">:</span> <span class="token punctuation">{</span>
    type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span>
    ele<span class="token punctuation">:</span> <span class="token string">&quot;div&quot;</span><span class="token punctuation">,</span>
    nodeIndex<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
    children<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> type<span class="token punctuation">:</span> <span class="token string">&quot;text&quot;</span><span class="token punctuation">,</span> value<span class="token punctuation">:</span> <span class="token string">&quot;123&quot;</span> <span class="token punctuation">}</span><span class="token punctuation">]</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  binding<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> type<span class="token punctuation">:</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">,</span> nodeIndex<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span> valueName<span class="token punctuation">:</span> <span class="token string">&quot;data&quot;</span> <span class="token punctuation">}</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre></div><p>这样，我们在生成一个 DOM 的时候，同时添加对<code>data</code>的监听，数据更新时我们会找到对应的<code>nodeIndex</code>，更新值：</p> <div class="language-js extra-class"><pre class="language-js"><code><span class="token comment">// 假设这是一个生成DOM的过程，包括数据绑定和</span>
<span class="token keyword">function</span> <span class="token function">generateDOM</span><span class="token punctuation">(</span><span class="token parameter">astObject</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> dom<span class="token punctuation">,</span> binding <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token operator">=</span> astObject<span class="token punctuation">;</span>
  <span class="token comment">// 生成DOM，这里假装当前节点是baseDom</span>
  baseDom<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token function">getDOMString</span><span class="token punctuation">(</span>dom<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token comment">// 对于数据绑定的，来进行监听更新吧</span>
  baseDom<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">&quot;data:change&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">name<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
    <span class="token comment">// 寻找匹配的数据绑定</span>
    <span class="token keyword">const</span> obj <span class="token operator">=</span> binding<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">x</span> <span class="token operator">=&gt;</span> x<span class="token punctuation">.</span>valueName <span class="token operator">==</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// 若找到值绑定的对应节点，则更新其值。</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      baseDom<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`[data-node-index=&quot;</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>obj<span class="token punctuation">.</span>nodeIndex<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&quot;]`</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> value<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">// 获取DOM字符串，这里简单拼成字符串</span>
<span class="token keyword">function</span> <span class="token function">getDOMString</span><span class="token punctuation">(</span><span class="token parameter">domObj</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// 无效对象返回''</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>domObj<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> type<span class="token punctuation">,</span> children <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> nodeIndex<span class="token punctuation">,</span> ele<span class="token punctuation">,</span> value <span class="token punctuation">}</span> <span class="token operator">=</span> domObj<span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">==</span> <span class="token string">&quot;dom&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment">// 若有子对象，递归返回生成的字符串拼接</span>
    <span class="token keyword">const</span> childString <span class="token operator">=</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">;</span>
    children<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">x</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
      childString <span class="token operator">+=</span> <span class="token function">getDOMString</span><span class="token punctuation">(</span>x<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// dom对象，拼接生成对象字符串</span>
    <span class="token keyword">return</span> <span class="token template-string"><span class="token string">`&lt;</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ele<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> data-node-index=&quot;</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nodeIndex<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&quot;&gt;</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>childString<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ele<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&gt;`</span></span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">==</span> <span class="token string">&quot;text&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment">// 若为textNode，返回text的值</span>
    <span class="token keyword">return</span> value<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p>我们来对上面的代码进行说明。</p> <p><strong>1. 根据节点信息生成对应的<code>HTML string</code>，也即<code>getDOMString()</code>方法。</strong></p> <p>这里我们只是简单完成了一种实现方式，根据节点生成 DOM 也有其他方式，例如使用<code>.createElement()</code>、<code>.appendChild()</code>、<code>textContent</code>等等。</p> <p>我们称通过生成<code>HTML string</code>的方式为<strong>字符串模版</strong>，同时我们将通过<code>createElement()/appendChild()</code>的方式生成 DOM 称为<strong>节点模版</strong>。</p> <p><strong>2. 通过监听数据变更，同时根据绑定的数值获取对应节点，并进行局部更新。</strong></p> <p>在使用字符串模版的时候，我们将<code>nodeIndex</code>绑定在元素属性上，主要是用于数据更新时追寻节点进行内容更新。
在使用节点模版的时候，我们可在创建节点的时候，将该节点保存下来，直接用于数据更新。</p> <p>当然，即使在字符串模版，我们也可以遍历一遍<code>binding</code>来获取所有绑定数据的节点并保存，这样就不用每次数据更新事件触发的时候重新进行获取，毕竟 DOM 节点的匹配也是会有一定的消耗的。</p> <p><strong>3. 无论是数据还是事件、属性、样式等的绑定，都可以通过相似的方法进行。</strong></p> <p>虽然这里我们只介绍了数据的绑定，但其实事件的绑定、属性和样式的绑定都可以用相似的方式进行，当然事件监听和事件的触发都是我们自己定义的，对于传递的内容都可以用自己想要的方式来传。</p> <h2 id="ast-生成模版"><a href="#ast-生成模版" aria-hidden="true" class="header-anchor">#</a> AST 生成模版</h2> <h3 id="生成模版的方法"><a href="#生成模版的方法" aria-hidden="true" class="header-anchor">#</a> 生成模版的方法</h3> <p>我们在捕获得到一个 AST 树结构后，会将其生成对应的 DOM。一般来说我们有这些方式：</p> <ol><li><strong>字符串模版：使用拼接的方式生成 DOM 字符串，直接通过<code>innderHTML()</code>插入页面。</strong></li> <li><strong>节点模版：使用<code>createElement()/appendChild()/textContent</code>等方法，动态地插入 DOM 节点，根节点使用<code>appendChild()</code>插入页面。</strong></li> <li><strong>混合模版：使用<code>createElement()/appendChild()/textContent</code>等方法动态地插入 DOM 节点，但是根节点使用<code>innderHTML()</code>插入页面。</strong></li></ol> <p>这几个有什么区别呢？</p> <p>刚开始的时候，我们每次更新页面数据和状态，通常通过<code>innerHTML</code>方法来用新的<code>HTML String</code>替换旧的，这种方法写起来很简单，无非是将各种节点使用字符串的方式拼接起来而已。但是如果我们更新的节点范围比较大，这时候我们需要替换掉很大一片的<code>HTML String</code>。</p> <p>对于浏览器，这样的一次<code>HTML String</code>替换并不只是更新一些字符串那么简单。</p> <h3 id="浏览器的渲染机制"><a href="#浏览器的渲染机制" aria-hidden="true" class="header-anchor">#</a> 浏览器的渲染机制</h3> <p>浏览器的一次页面渲染其实开销并不小，首先浏览器会解析三种文件：</p> <ul><li>解析<code>HTML/SVG/XHTML</code>，会生成一个<code>DOM结构树</code></li> <li>解析<code>CSS</code>，会生成一个<code>CSS规则树</code></li> <li>解析<code>JS</code>，可通过<code>DOM API</code>和<code>CSS API</code>来操作<code>DOM结构树</code>和<code>CSS规则树</code></li></ul> <p><code>CSS规则树</code>与<code>DOM结构树</code>结合，最终生成一个<code>Render树</code>（即最终呈现的页面，例如其中会移除<code>DOM结构树</code>中匹配到<code>CSS</code>里面<code>display:none;</code>的 DOM 节点）。其中，<code>CSS</code>匹配 DOM 结构的过程是很复杂的，曾经在机器配置不高的日子也会出现过性能问题。</p> <p>**一般来说浏览器绘制页面的过程是：1.计算<code>CSS规则树</code> =&gt; 2.生成<code>Render树</code> =&gt; 3.计算各个节点的<code>大小</code>/<code>position</code>/<code>z-index</code> =&gt; 4.绘制。**其中计算的环节也是消耗较大的地方。</p> <p>我们使用<code>DOM API</code>和<code>CSS API</code>的时候，通常会触发浏览器的两种操作：<code>Repaint</code>和<code>Reflow</code>。</p> <p>**Repaint：页面部分重画，通常不涉及尺寸的改变，常见于颜色的变化。**这时候一般只触发绘制过程的第 4 个步骤。</p> <p>**Reflow：意味着节点需要重新计算和绘制，常见于尺寸的改变。**这时候会触发 3 和 4 两个步骤。</p> <p>所以我们在写页面的时候会注意一些问题，例如不要一条一条地修改 DOM 的样式（会触发多次的计算或绘制），在写动画的时候多使用<code>fixed</code>/<code>absolute</code>等（<code>Reflow</code>的范围小），等等。</p> <p>回到话题，如果我们直接每次更新页面数据和状态，都使用<code>innerHTML</code>的方式，无疑会增加浏览器的负担，所以需要跟踪节点进行局部跟新。当然，<code>innerHTML</code>也有它的优势，那就是我们可以使用一个<code>innerHTML</code>替代很多很多的<code>createElement()/appendChild()/textContent</code>方法，这在我们较少使用数据绑定和更新的情况下高效得多。</p> <h1 id="模版数据更新"><a href="#模版数据更新" aria-hidden="true" class="header-anchor">#</a> 模版数据更新</h1> <p>我们讲了模版生成 AST，以及通过 AST 生成 DOM、并进行数据绑定的过程，接下来说明下模版数据更新的过程。</p> <h3 id="数据更新监听"><a href="#数据更新监听" aria-hidden="true" class="header-anchor">#</a> 数据更新监听</h3> <p>前面将数据绑定的时候，也讲了使用<strong>事件监听</strong>的方式监听数据更新。这里接着介绍一些其他的方式。</p> <p><strong>脏检测：在 Angular 中，并不直接监听数据的变动，而是监听常见的事件如用户交互（点击、输入等）、定时器、生命周期等。在每次事件触发完毕后，计算数据的新值和旧值是否有差异，若有差异则更新页面，并触发下一次的脏检测，直到没有差异或是次数达到设定阈值。</strong></p> <p>脏检测是 Angular 的一大特色。由于事件触发的时候，并不能知道哪些数据会有变化，所以会进行大面积数据的新旧值 Diff，这也毫无疑问会导致一些性能问题。在 Angular2 版本之后，由于使用了<code>zone.js</code>对异步任务进行跟踪，把这个计算放进 worker，完了更新回主线程，是个类似多线程的设计，也提升了性能。</p> <p>同时，在 Angular2 中应用的组织类似 DOM，也是树结构的，脏检查会从根组件开始，自上而下对树上的所有子组件进行检查。相比 Angular1 中的带有环的结构，这样的单向数据流效率更高，而且容易预测。</p> <p><strong>Getter/Setter：在 Vue 中，主要是使用<code>Proxy</code>的方式，在相关的数据写入时进行模版更新。</strong></p> <p><strong>手动 Function：在 React 中，通过手动调用<code>set()</code>的方式写入数据来更新模版。</strong></p> <p>使用<code>Proxy</code>或者是<code>set()</code>的时候，我们可以通过<code>event emit</code>或是<code>callback</code>回调的方法，来触发数据的计算以及模版的更新。</p> <h3 id="数据更新-diff"><a href="#数据更新-diff" aria-hidden="true" class="header-anchor">#</a> 数据更新 Diff</h3> <p>说到数据更新的 Diff，更多的则是<code>Diff + 更新模板</code>这样一个过程。</p> <p>在这个过程中，最突出的也就是虚拟 DOM，它解决了常见的局部数据更新的问题，例如数组中值位置的调换、部分更新。一般来说计算过程如下：</p> <p><strong>1. 用 JS 对象模拟 DOM 树。</strong></p> <p>不知道大家仔细研究过 DOM 节点对象没，一个真正的 DOM 元素非常庞大，拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的，所以我们的第一步就是简化 DOM 对象。
我们用一个<code>JavaScript</code>对象结构表示 DOM 树的结构，然后用这个树构建一个真正的 DOM 树。</p> <p><strong>2. 比较两棵虚拟 DOM 树的差异。</strong></p> <p>当状态变更的时候，重新构造一棵新的对象树。然后用新的树和旧的树进行比较，记录两棵树差异。通常来说这样的差异需要记录：</p> <ul><li>需要替换掉原来的节点</li> <li>移动、删除、新增子节点</li> <li>修改了节点的属性</li> <li>对于文本节点的文本内容改变</li></ul> <p>经过差异对比之后，我们能获得一组差异记录，接下里我们需要使用它。</p> <p><strong>3. 把差异应用到真正的 DOM 树上。</strong></p> <p>对差异记录要应用到真正的 DOM 树上，例如节点的替换、移动、删除，文本内容的改变等。</p> <h1 id="结束语"><a href="#结束语" aria-hidden="true" class="header-anchor">#</a> 结束语</h1> <p>总的来说，一个前端模板引擎大致分为<code>模板生成AST =&gt; AST生成模板 =&gt; 数据/事件/属性绑定的监听 =&gt; 数据变更Diff =&gt; 局部更新模板</code>这些过程。当然上面的介绍以个人理解为主，部分源码验证为辅。<br>
还是那句话，多思考多总结，不管结论是否正确，结果是否所期望，过程中的收获也会让人成长。</p></div> <!----> <footer class="page-edit"><div class="edit-link"><a href="https://github.com/godbasin/front-end-playground/edit/sourcecode/docs/front-end-basic/understanding/template-engine.md" target="_blank" rel="noopener noreferrer">帮阿猪改善此页面！</a> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></div> <!----> <blockquote>部分文章中使用了一些网站的截图，如果涉及侵权，请告诉我删一下谢谢~</blockquote> <div style="margin-top:30px;"><div class="el-row" style="margin-left:-10px;margin-right:-10px;"><div class="el-col el-col-24 el-col-sm-0 el-col-md-2 el-col-lg-4" style="padding-left:10px;padding-right:10px;display:block;"><div style="width:1px;height:1px;"></div></div> <div class="el-col el-col-24 el-col-sm-24 el-col-md-18 el-col-lg-16" style="padding-left:10px;padding-right:10px;"><div class="el-card box-card is-always-shadow"><div class="el-card__header"><div class="clearfix"><span>温馨提示喵</span></div></div><div class="el-card__body"> <div class="el-row" style="margin-left:-10px;margin-right:-10px;"><div class="el-col el-col-24 el-col-xs-24 el-col-sm-12" style="padding-left:10px;padding-right:10px;"><div class="el-image"><div class="image-slot"><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/assets/img/loading.gif" style="width:100%;"></div><!----></div></div> <div class="el-col el-col-24 el-col-xs-24 el-col-sm-12" style="padding-left:10px;padding-right:10px;"><div class="copyright-text"><div>本文版权归作者所有，欢迎转载，但未经作者同意必须保留此段声明，且在文章页面明显位置给出原文连接，否则保留追究法律责任的权利。</div> <div>出处：被删的前端游乐场</div> <div>作者：<a href="https://github.com/godbasin" target="_blank">被删</a></div></div></div></div></div></div></div></div></div></footer> <div class="page-nav"><p class="inner"><span class="prev">
        ←
        <a href="/front-end-playground/front-end-basic/understanding/app-component-isolation.html" class="prev">
          页面区块化与应用组件化
        </a></span> <span class="next"><a href="/front-end-playground/front-end-basic/understanding/dialogue-abstraction.html">
          对话抽象
        </a>
        →
      </span></p></div>  <div class="gitalk-container theme-default-content"><div id="gitalk-container" class="content"></div></div></main> <div id="kitty-container"><span><div role="tooltip" id="el-popover-6114" aria-hidden="true" class="el-popover el-popper" style="width:undefinedpx;display:none;"><!----><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/2code2.jpg" class="image"> <div class="text">牡羊猪的猫粮罐</div> </div><div id="kitty" style="background:url(https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/assets/img/kitty1.svg);"></div></span> <div class="el-dialog__wrapper" style="display:none;"><div role="dialog" aria-modal="true" aria-label="牡羊猪是这样渐渐胖成猪的喵（点击图片可以切换噢）" class="el-dialog" style="margin-top:15vh;"><div class="el-dialog__header"><span class="el-dialog__title">牡羊猪是这样渐渐胖成猪的喵（点击图片可以切换噢）</span><button type="button" aria-label="Close" class="el-dialog__headerbtn"><i class="el-dialog__close el-icon el-icon-close"></i></button></div><!----><!----></div></div></div></div><div class="global-ui"></div></div>
    <script src="/front-end-playground/assets/js/app.e5cf4092.js" defer></script><script src="/front-end-playground/assets/js/3.c853767c.js" defer></script><script src="/front-end-playground/assets/js/2.eed49fdd.js" defer></script><script src="/front-end-playground/assets/js/45.48747600.js" defer></script>
  </body>
</html>
